package com.king.mind;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

import javafx.animation.AnimationTimer;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import lombok.extern.slf4j.Slf4j;

/**
 * mind 画布
 * 
 * @author king
 * @date 2025年6月12日-13:10:52
 */
@Slf4j
public class Mind extends Canvas {

	/**
	 * X轴偏移量
	 */
	private double offsetX = 0;

	/**
	 * Y轴偏移量
	 */
	private double offsetY = 0;

	/**
	 * 缩放量
	 */
	private double scale = 1;

	/**
	 * 网格点大小
	 */
	private byte gridPointSize = 2;

	/**
	 * 网格间距，正方形
	 */
	private byte gridSpacing = 20;

	/**
	 * 记录鼠标位置
	 */
	private double lastMouseX = 0;
	private double lastMouseY = 0;

	/**
	 * 拖动状态
	 */
	private boolean dragIs = false;

	private AnimationTimer animation;

	/**
	 * 画布平移动画
	 */
	private AnimationTimer linearAnimation;

	/**
	 * 拖动的速度
	 */
	private double velocityX = 0;
	private double velocityY = 0;
	/**
	 * 阻尼系数
	 */
	private float damping = .9F;

	/**
	 * 章节节点
	 */
	private List<Node> mindNodes = new ArrayList<>();

	/**
	 * 网格颜色
	 */
	private int gridColor = 0x1dFFFFFF;

	/**
	 * 每行显示多少个node
	 */
	private byte lineNum = 10;

	double startX;
	double startY;

	private GraphicsContext g = getGraphicsContext2D();

	private String curNodeId;

	private Consumer<String> callback;

	/**
	 * 键盘除法时回调
	 */
	private Consumer<String> keyDownInfo;

	private StringBuilder numKey;

	/**
	 * 
	 * @param callback    这是双击选中节点是的回调
	 * @param keyDownInfo 按下键时回调，因为Mind是纯Canvas画布，不打算使用期绘制按下了哪些键，所以这个回调出去，使用外部组件来实现显示功能
	 * @author king
	 * @date 2025年7月14日-16:20:55
	 */
	public Mind(Consumer<String> callback, Consumer<String> keyDownInfo) {
		this.callback = callback;
		this.keyDownInfo = keyDownInfo;
		// 处理尺寸变化时重新绘制
		widthProperty().addListener((_)->{
			draw();
		});
		heightProperty().addListener((_)->{
			draw();
		});

		addEventHandler(ScrollEvent.ANY, this::onScale);
		addEventHandler(MouseEvent.MOUSE_DRAGGED, this::onDrag);
		addEventHandler(MouseEvent.MOUSE_PRESSED, this::onMousePress);
		addEventHandler(MouseEvent.MOUSE_RELEASED, this::onMouseRelease);

		// 添加键盘事件
		addEventHandler(KeyEvent.KEY_RELEASED, this::onKeyUp);
		numKey = new StringBuilder();

	}

	/**
	 * 键盘事件
	 * 
	 * @param e
	 * @author king
	 * @date 2025年7月14日-16:03:19
	 */
	private void onKeyUp(KeyEvent e) {
		// 这里的快键键先不处理辅助键先
		if (e.isAltDown() || e.isConsumed() || e.isControlDown() || e.isMetaDown() || e.isShiftDown()
				|| e.isShortcutDown()) {
			return;
		}

		if (e.getCode() == KeyCode.S) {
			// 按下S（Start单词缩写），便将偏移量设置为0，但是如果直接将偏移量设置为0，非常僵硬，需要牺牲一些计算来实现平滑移动效果
			if (offsetX != 0 && offsetY != 0) {
				doneLinearAnimation(0, 0);
				keyDownInfo.accept("S 键，将会回到起点");
			}
			// 按下非数字的，那么numKey设置0，即：清空
			numKey.setLength(0);
		} else if (e.getCode() == KeyCode.E) {
			if (this.mindNodes.size() != 0) {
				doneLinearAnimation(this.mindNodes.getLast());
				keyDownInfo.accept("E 键，来到最后一章");
			}
		} else if (e.getText().matches("\\d")) {
			doneChapterNum(e.getText());
		} else if (KeyCode.ENTER.equals(e.getCode())) {
			if (numKey.length() != 0) {
				int num = Math.min(this.mindNodes.size() - 1, Integer.valueOf(numKey.toString()));
				doneLinearAnimation(this.mindNodes.stream().filter(n -> {
					return n instanceof ChapterNode;
				}).toList().get(num - 1));
				numKey.setLength(0);
			}
		} else if (KeyCode.ESCAPE.equals(e.getCode())) {
			numKey.setLength(0);
			keyDownInfo.accept("ESC重置键位");
		}
	}

	/**
	 * 处理要定位的章节
	 * 
	 * @param num
	 * @author king
	 * @date 2025年7月15日-14:41:37
	 */
	private void doneChapterNum(String num) {
		List<Node> nodes = this.mindNodes.stream().filter(n -> {
			return n instanceof ChapterNode;
		}).toList();
		
		if (Integer.valueOf(numKey.toString() + num) > nodes.size()) {
			numKey.setLength(0);
			numKey.append(nodes.size());
		} else {
			numKey.append(num);
		}
		String title = ((ChapterNode) nodes.get(Integer.valueOf(numKey.toString()) - 1)).getTitle();
		keyDownInfo.accept(String.format("回车可跳转到： %s", title));
	}

	/**
	 * 平移到指定节点附近，没有做具体计算，如果要做精确定位，那么需要处理所在章节
	 * 
	 * @param node
	 * @author king
	 * @date 2025年7月15日-13:23:48
	 */
	private void doneLinearAnimation(Node node) {
		double x = (getWidth() - node.getWidth()) / 2;
		double y = (getHeight() - node.getHeight()) / 2;
		doneLinearAnimation(x - node.getX(), y - node.getY());
	}

	/**
	 * 整个画布平移动画效果
	 * 
	 * @param x 目标X
	 * @param y 目标Y
	 * @author king
	 * @date 2025年7月14日-16:09:23
	 */
	private void doneLinearAnimation(double x, double y) {
		// 记录当前偏移量
		double startX = offsetX;
		double startY = offsetY;
		// 过渡所用时间
		double duration = .5d;

		/**
		 * 开始时间，用来记录动画开始的事件，尽量精确，使用纳秒
		 */
		double startTime = System.nanoTime();

		linearAnimation = new AnimationTimer() {
			@Override
			public void handle(long now) {
				// 开始时间的一个过渡情况值，这个值我们将其转成秒来进行比较，因为在我们正常人接触的时间最小单位就是秒了，这个duration可以使用配置形式传入，这里我就不处理
				double startToNowSec = (now - startTime) / 1e9;
				// 时间进度
				double progress = Math.min(startToNowSec / duration, 1.0);

				// 核心，线性插值公式
				offsetX = startX + (x - startX) * progress;
				offsetY = startY + (y - startY) * progress;
				if (progress >= 1.0) {
					// 如果精度大于等于1的时候，表示时间到了，因为除法中，分母就是我们的过渡时长，但是一般除法没有那么精确，刚好抵达我们的目标X，Y值，所以这里需要精确赋值一下
					offsetX = x;
					offsetY = y;
					linearAnimation.stop();
				}
				draw();
			}
		};
		linearAnimation.start();
	}

	/**
	 * 用于外部调用，向画布添加节点，如：从数据库来的数据
	 * 
	 * @param nodes
	 * @author king
	 * @date 2025年6月17日-11:12:58
	 */
	public void addNodes(Node... nodes) {
		double width = NodeProperty.WIDTH;
		// 最后一卷的章节数量
		int lastVolumeChapterNum = 0;
		for (int i = this.mindNodes.size() - 1; i >= 0; i--) {
			if (this.mindNodes.get(i) instanceof VolumeNode) {
				break;
			}
			lastVolumeChapterNum++;
		}

		/*
		 * 以下算法适合初始时一次性添加多个节点，且没有处理偏移量和缩放时的数据 lineNum是每行显示3个，这里是方便我测试所以写了3,
		 * 这里使用我们的chapterNode的数量对每行数量进行取模，就是要判断当前行是否满行，
		 */
		int module = lastVolumeChapterNum % this.lineNum;

		Node box = new Node();
		double maxHeight = doneLastLineNode(box, module);
		startX = box.getX();
		startY = box.getY();

		// 用于记录一行中，chapterNode的数量值，当遇到VolumeNode时，恢复为0，表示从0开始了
		int i = module;
		// 实际的节点数量计数
		// 最后一个节点
		Node lastNode = null;
		if (this.mindNodes.size() != 0) {
			lastNode = this.mindNodes.getLast();
		}

		/*
		 * 来到这里，我们就是循环，当然因为要计算X值，所以使用上面的模
		 */
		for (Node n : nodes) {
			if (n instanceof VolumeNode) {
				startX = gridSpacing;
				startY += maxHeight + gridSpacing;
				maxHeight = n.getHeight();
				i = 0;
			} else {
				if (i % this.lineNum == 0) {
					startX = gridSpacing;
					startY += maxHeight + gridSpacing;
					// 换行了，那么最大高度也将重新定义，从第一个开始
					maxHeight = n.getHeight();
				} else {
					// X的坐标是上一个的坐标+宽度，在加上网格间距
					if (lastNode instanceof VolumeNode) {
						startX = gridSpacing;
						startY = lastNode.getY() + gridSpacing + lastNode.getHeight();
					} else {
						startY = lastNode.getY();
						startX = lastNode.getX() + lastNode.getWidth() + gridSpacing;
					}
				}
				i++;
				n.setWidth(width);
			}
			maxHeight = Math.max(maxHeight, n.getHeight());
			n.setXY(startX, startY);
			lastNode = n;
		}
		// drawRect中有频繁的修改画笔，故存个档，避免无法还原
		drawRect(nodes);
		this.mindNodes.addAll(Arrays.asList(nodes));
		requestFocus();
		if (nodes.length == 1) {
			// 如果是一个一个添加的，那么定位到添加的节点处
			select(nodes[0]);
		}
	}

	/**
	 * 绘制矩形，目前值考虑了基本的矩形，后续在进行区分title，subTitle，label的绘制，都是在这里进行处理
	 * 
	 * @param nodes
	 * @author king
	 * @date 2025年6月17日-11:28:09
	 */
	private void drawRect(Node... nodes) {
		ArrayList<ChapterNode> chapters = new ArrayList<>(nodes.length);
		ArrayList<VolumeNode> volumes = new ArrayList<>(nodes.length);
		for (Node n : nodes) {
			if (n instanceof ChapterNode) {
				chapters.add((ChapterNode) n);
			} else if (n instanceof VolumeNode) {
				volumes.add((VolumeNode) n);
			}
		}
		if (chapters.size() > 0) {
			g.save();
			drawChapterNode(chapters.toArray(new ChapterNode[0]));
			g.restore();
		}
		if (volumes.size() > 0) {
			g.save();
			drawVolumeNode(volumes.toArray(new VolumeNode[0]));
			g.restore();
		}
	}

	private void drawVolumeNode(VolumeNode... nodes) {
		double textHeight = g.getFont().getSize();
		g.setFont(Font.font(textHeight * scale));
		// 文本高度，使用缩放后的
		textHeight = g.getFont().getSize();
		Font font = g.getFont();
		for (VolumeNode n : nodes) {
			g.setFill(color(NodeProperty.VOLUME_BG_COLOR));
			g.fillRect(n.getX() * scale, (n.getY() + offsetY) * scale, n.getWidth() * scale, n.getHeight() * scale);
			{
				g.setFill(color(NodeProperty.TITLE_TEXT_COLOR));
				g.setFont(Font.font("", FontWeight.BOLD, g.getFont().getSize() * 1.5));
				g.fillText(n.getTitle(), (n.getX() + gridSpacing) * scale,
						(n.getY() + offsetY + n.getTitleHeight() / 2) * scale + textHeight / 2);
				g.setFont(font);

				g.fillText(n.getSubTitle(), (n.getX() + gridSpacing) * scale,
						(n.getY() + offsetY + n.getTitleHeight() + (n.getTitleHeight()) / 2) * scale);
			}
		}
	}

	private void drawChapterNode(ChapterNode... nodes) {
		double scaleTitleHeight = NodeProperty.TITLE_HEIGHT * scale;
		double scaleSubTitleHeight = NodeProperty.SUB_TITLE_HEIGHT * scale;
		double scaleLabelHeight = NodeProperty.LABEL_HEIGHT * scale;
		// 修改字体大小，是无法还原的，所以先给画笔存档
		double textHeight = g.getFont().getSize();
		g.setFont(Font.font(textHeight * scale));
		// 文本高度，使用缩放后的
		textHeight = g.getFont().getSize();
		ChapterNode n;
		double sx, sy, tsy, sw, sh;
		Font font = g.getFont();

		for (int i = 0; i < nodes.length; i++) {
			n = nodes[i];
			// 绘制title
			sx = (n.getX() + offsetX) * scale;
			sy = (n.getY() + offsetY) * scale;
			sw = n.getWidth() * scale;
			sh = n.getHeight() * scale;
			if (n.isSelected()) {
				g.setStroke(color(NodeProperty.BORDER_COLOR));
				double size = 20 * scale;
				double lw = 4 * scale;
				double hlw = lw / 2;
				g.setLineWidth(lw);
				// 横向线
				{
					// 左上
					g.strokeLine(sx + hlw, sy - hlw, sx + size, sy - hlw);
					// 右上
					g.strokeLine(sx + sw - hlw, sy - hlw, sx + sw - size, sy - hlw);
					// 左下
					g.strokeLine(sx + hlw, sy - hlw + sh + lw, sx + size, sy - hlw + sh + lw);
					// 右下
					g.strokeLine(sx + sw - hlw, sy + sh + hlw, sx + sw - size, sy + sh + hlw);
				}
				// 纵向线
				{
					// 左上
					g.strokeLine(sx - hlw, sy - hlw, sx - hlw, sy + size);
					// 右上
					g.strokeLine(sx + sw + hlw, sy - hlw, sx + sw + hlw, sy + size);
					// 左下
					g.strokeLine(sx - hlw, sy + sh + hlw, sx - hlw, sy + sh - size);
					// 右下
					g.strokeLine(sx + sw + hlw, sy + sh + hlw, sx + sw + hlw, sy + sh - size);

				}

			}
			g.setLineWidth(scale);
			tsy = sy+(scaleTitleHeight + textHeight) / 2;
			{
				// 绘制title背景
				g.setFill(color(NodeProperty.TITLE_BG_COLOR));
				g.fillRect(sx, sy, sw, scaleTitleHeight);

				// 绘制title文本
				g.setFill(color(NodeProperty.TITLE_TEXT_COLOR));
				// 标题加粗
				g.setFont(Font.font("", FontWeight.MEDIUM, g.getFont().getSize() * 1.3));
				// 文本的位置，应在title矩形框水平居中位置，距离左侧1/2gridSpacing，且需要考虑文本的缩放，文本自身也有高度
				// 为每个字特地安排X坐标，实现文字间距自由控制，字间距：20
				g.fillText(n.getTitle(), sx + (gridSpacing / 2) * scale, tsy);
				g.setFont(font);
			}
			// 绘制subTitle，subTitle是在title之下，所以其Y值要添加title的高度
			{
				// 绘制subtitle背景
				g.setFill(color(NodeProperty.SUB_TITLE_BG_COLOR));
				g.fillRect(sx, sy + scaleTitleHeight, sw, scaleSubTitleHeight);

				// 绘制subtitle文本
				g.setFill(color(NodeProperty.SUB_TITLE_TEXT_COLOR));
				// 文本的位置，应在title矩形框水平居中位置，距离左侧1/2gridSpacing，且需要考虑文本的缩放，文本自身也有高度
				g.fillText(n.getSubTitle(), sx + gridSpacing / 2 * scale,
						sy + (scaleSubTitleHeight + textHeight) / 2 + scaleTitleHeight);
			}
			// 绘制labels，这是一个数组
			{
				if (n.getLabels() != null && n.getLabels().length > 0) {
					// label在title，subTitle之下
					double startLabelY = scaleTitleHeight + scaleSubTitleHeight;
					for (String label : n.getLabels()) {
						// 绘制矩形背景
						g.setFill(color(NodeProperty.LABEL_BG_COLOR));
						g.fillRect(sx, sy + startLabelY, sw, scaleLabelHeight);

						// Label画一条下划线
						g.setStroke(color(NodeProperty.LABEL_UNDER_LINE_COLOR));
						g.strokeLine(sx, sy + startLabelY + scaleLabelHeight,
								sx + sw,
								sy + startLabelY + scaleLabelHeight);

						g.setFill(color(NodeProperty.LABEL_TEXT_COLOR));
						g.fillText(label, sx + gridSpacing / 2 * scale,
								sy
								+ (scaleLabelHeight + textHeight) / 2 + startLabelY);
						startLabelY += scaleLabelHeight;
					}
				}
			}
		}
	}

	/**
	 * 鼠标释放时处理一些逻辑，如：拖动的阻尼效果
	 * 
	 * @param e
	 * @author king
	 * @date 2025年6月12日-13:43:54
	 */
	private void onMouseRelease(MouseEvent e) {
		if (e.getButton() == MouseButton.PRIMARY) {
			// 要处理的是拖动，所以需要一个变量来记录拖动状态
			if (dragIs) {
				// 释放鼠标，那么此处将其状态设置为非拖动状态
				dragIs = false;
				// 阻尼效果需要使用动画类来处理
				animation = new AnimationTimer() {
					@Override
					public void handle(long now) {
						// 处理阻尼，那么就需要记录拖动的一些数据，如：速度，系数
						offsetX += velocityX;
						offsetY += velocityY;
						velocityX *= damping;
						velocityY *= damping;
						// 动画有启动，那么就有停止，我们在这个系数很小的时候就将其停止
						// 拖动时，明显可以感觉到，从左往右拖动时，没有问题，但是从右边向左边拖动就停住了，所以这里要处理方向问题
						if (Math.abs(velocityX) < .1 && Math.abs(velocityY) < .1) {
							stop();
						}
						draw();
					}
				};
				animation.start();
			}
		}
	}

	/**
	 * 鼠标按下
	 * @param e
	 * @author king
	 * @date 2025年6月12日-13:41:15
	 */
	private void onMousePress(MouseEvent e) {
		if (e.getButton() == MouseButton.PRIMARY) {
			// 只处理鼠标左键,这里不给lastMouseX,Y 赋值，如果是缩小的情况下，看不出问题，但是放大后，会发现拖动的物体是跳动的
			lastMouseX = e.getX();
			lastMouseY = e.getY();

			if (e.getClickCount() >= 2) {
				select(e.getX(), e.getY());
			}
		}
		// 当点击canvas时，canvas获取焦点，否则其将无法接收键盘事件
		requestFocus();
	}

	/**
	 * 
	 * @param node
	 * @author king
	 * @date 2025年7月18日-17:05:41
	 */
	private void select(Node node) {
		for (Node n : this.mindNodes) {
			if (n.isSelected()) {
				n.setSelected(false);
				break;
			}
		}
		node.setSelected(true);
		callback.accept(node.getId());
		doneLinearAnimation(node);
	}

	/**
	 * 双击选中
	 * 
	 * @param x 鼠标X
	 * @param y
	 * @author king
	 * @date 2025年7月10日-15:24:43
	 */
	private void select(double x, double y) {
		boolean findTrue = false;
		boolean find = false;
		Node lastSelectedNode = null;
		for (Node n : this.mindNodes) {
			if (n.isSelected()) {
				findTrue = true;
				lastSelectedNode = n;
			}
			n.setSelected(false);
			if (!find && x >= (n.getX() + offsetX) * scale && x <= (n.getX() + n.getWidth() + offsetX) * scale
					&& y >= (n.getY() + offsetY) * scale && y <= (n.getY() + offsetY + n.getHeight()) * scale) {
				find = true;
				n.setSelected(find);

				if (callback != null) {
					callback.accept(n.getId());
				}
				if (findTrue) {
					// 其实这里只刷新当前的节点，从优化方面来说，这里其实只需要重绘当前区域即可，但是没有分层，所以只能全部重绘了
					if (curNodeId == null || !curNodeId.equals(n.getId())) {
						draw();
					}
					curNodeId = n.getId();
					return;
				}
				curNodeId = n.getId();
			}
			if (findTrue && find) {
				// 其实这里只刷新当前的节点，从优化方面来说，这里其实只需要重绘当前区域即可，但是没有分层，所以只能全部重绘了
				draw();
				return;
			}
		}
		// 如果没有选中任何节点，且上次有选中过，那么此次双击将其还原为选中，不能双击后空白处就将其设置为未选中了。
		if (!find && lastSelectedNode != null) {
			lastSelectedNode.setSelected(true);
		}
		// 能来到这里，说明之前都没有选中过任何一个Node
		draw();
	}

	/**
	 * 绘制网格
	 * 
	 * @author king
	 * @date 2025年6月12日-13:11:43
	 */
	private void drawGrid() {
		// 绘制的网格尺寸与画布大小保持一致
		double width = getWidth();
		double height = getHeight();
		/*
		 * 因为使用width和height监听会出调用绘制方法，那么在应用启动时，画布尺寸应该是0,0，那么这种情况就不用执行后续逻辑了
		 */
		if (width == 0 || height == 0) {
			return;
		}
		// 考虑缩放
		double scaleSize = gridPointSize * scale;
		double scaleSpacing = gridSpacing * scale;
		// 起始位置是0，但是考虑偏移量
		double x = offsetX % scaleSpacing;
		double y;

		double tmpY = offsetY % scaleSpacing;
		// 准备绘制前先设置颜色
		g.setFill(color(gridColor));
		for (; x < width; x += scaleSpacing) {
			y = tmpY;
			for (; y < height; y += scaleSpacing) {
				g.fillOval(x, y, scaleSize, scaleSize);
			}
		}
	}

	/**
	 * 处理缩放，这是一个鼠标滚动事件，考虑偏移量和缩放大小
	 * 
	 * @author king
	 * @date 2025年6月12日-13:12:31
	 */
	private void onScale(ScrollEvent e) {
		// 处理缩放，主要便是修改scale的值
		double deltaY = e.getDeltaY();
		/*
		 * 缩放比例
		 */
		float zoom = 1.1F;
		if (deltaY < 0) {
			scale /= zoom;
		} else {
			scale *= zoom;
		}
		draw();
	}

	/**
	 * 处理拖拽，处理偏移量，这个也是一个鼠标事件
	 * 
	 * @author king
	 * @date 2025年6月12日-13:12:11
	 */
	private void onDrag(MouseEvent e) {
		// 仅处理鼠标左键，处理拖拽，主要是处理偏移量
		if (e.getButton() == MouseButton.PRIMARY) {
			dragIs = true;
			double x = e.getX() - lastMouseX;
			double y = e.getY() - lastMouseY;
			// 处理缩放后，移动量的问题，观察可以发现，当缩小后，移动量太小，不方便，所以处理一下移动量的问题
			byte moveDPI = 1;
			// 移动量便是因为缩放后出现的不适情况，所以与scale有关，这里我尝试了几个数值，可参考
			if (scale > .8) {
				moveDPI = 2;
			} else if (scale > .6) {
				moveDPI = 4;
			} else if (scale > .4) {
				moveDPI = 6;
			} else if (scale > .1) {
				moveDPI = 8;
			} else if (scale < 0.1) {
				moveDPI = 20;
			}

			offsetX += x * moveDPI;
			offsetY += y * moveDPI;

			velocityX = x * damping;
			velocityY = y * damping;

			lastMouseX = e.getX();
			lastMouseY = e.getY();

			draw();
		}
	}

	/**
	 * 这个方法用于处理上一个Node的坐标及上一行的最大height，因为我们要布局。
	 * 布局的规则就是，每行数量固定，达到值后换行，下一行的Y坐标是上一行的最大行+gridSpacing
	 * 
	 * @param node   这个Node就是用来接收x,y坐标的
	 * @param module 最后一个卷的章节数对行数的取模
	 * @return 返回上一行的最大height，当然，如果是第一行，那么就没有上一行，那么就是0
	 * @author king
	 * @date 2025年6月17日-11:15:33
	 */
	private double doneLastLineNode(Node node, int module) {
		if(this.mindNodes.size()==0) {
			node.setXY(0, 0);
			return 0D;
		}

		// 判断最后一个是否是卷
		if (this.mindNodes.getLast() instanceof VolumeNode) {
			// 如果只卷，那么X值从0开始
			VolumeNode last = (VolumeNode) this.mindNodes.getLast();
			node.setXY(0, last.getY());
			return last.getHeight();
		}

		int size = this.mindNodes.size();
		// 上面已经处理了，当元素是第一个的情况
		ChapterNode[] ns;
		// 代码能走到这里，那么说明不可能是第一个，也就是module=0的情况，绝不会是第一个元素，所以不用考虑第一个的情况了
		if (module == 0) {
			node.setX(0D);
			/*
			 * 不用考虑第一个的情况，那么也不用考虑chapterNodes中没有元素的情况，那么能进入这个逻辑，那么必然是第N+1行的末尾
			 * 如：每行显示10个，那么能来到这里，必然是第二行，三行...的最后一个元素，所以我们直接截取数据中的倒数第lineNum个元素。
			 * 这个倒数N个元素的作用，就是用来计算要换行了，获取这一行的最大height值
			 */
			ns = this.mindNodes.subList(size - this.lineNum, size).toArray(new ChapterNode[0]);
			double maxHeight = 0D;

			node.setY(ns[0].getY());
			for (ChapterNode n : ns) {
				maxHeight = Math.max(maxHeight, n.getHeight());
			}
			return maxHeight;
		} else {
			// 这种情况便是没有达到满行的情况，那么直接获取其最后一个元素的X，Y值即可，高度不用考虑，直接返回0
			node.setX(this.mindNodes.getLast().getX());
			node.setY(this.mindNodes.getLast().getY());
			return 0D;
		}
	}

	/**
	 * 重新计算布局，并重新绘制整个画布，没有做优化，直接将数据全部重新处理
	 * 
	 * @param node 修改的节点
	 * @author king
	 * @date 2025年7月18日-17:21:01
	 */
	public void reDraw(ChapterNode node) {
		ChapterNode cn;
		for (Node n : this.mindNodes) {
			if (n.getId().equals(node.getId())) {
				cn = (ChapterNode) n;
				cn.setTitle(node.getTitle());
				cn.setLabels(node.getLabels());
				cn.setSubTitle(node.getSubTitle());
			}
		}
		List<Node> nodes = new ArrayList<>(this.mindNodes);
		this.mindNodes.clear();
		g.clearRect(0, 0, getWidth(), getHeight());
		addNodes(nodes.toArray(new Node[0]));
	}

	/**
	 * 删除最后一章
	 * 
	 * @param node
	 * @author king
	 * @date 2025年7月22日-09:48:21
	 */
	public void delNode(String nodeId) {
		Node node = null;
		for (Node n : this.mindNodes) {
			if (n.getId().equals(nodeId)) {
				node = n;
				break;
			}
		}
		if (node != null) {
			this.curNodeId = null;
			this.mindNodes.remove(node);
		}
		draw();
	}

	/**
	 * 绘制整个画布的内容
	 * 
	 * @author king
	 * @date 2025年6月12日-13:16:46
	 */
	private void draw() {
		g.save();
		g.clearRect(0, 0, getWidth(), getHeight());
		// 这里可以考虑一下绘制网格的情况，当缩放太小后，网格其实已经不方便查看了，所以直接不用浪费资源绘制它了
		if (scale > .4) {
			drawGrid();
		}
		// 这里考虑，以下因为draw的调用频率相当的高，所以必须判断一下，否则消费没必要的资源调用drawRect方法
		if (this.mindNodes.size() > 0) {
			drawRect(this.mindNodes.toArray(new Node[0]));
		}
		g.restore();
	}

	public void clear() {
		this.mindNodes.clear();
		this.offsetX = this.offsetY = 0;
		this.scale = 1;
		g.clearRect(0, 0, getWidth(), getHeight());
	}

	/**
	 * 颜色分量，fx有Color.web("#ccccccaa")这种写法的，但是这个有打量的字符串处理逻辑，我们的颜色计算比较频繁，所以不使用这种方式
	 * 自己写颜色分量，颜色提取后，透明度需要自己手动归一化
	 * 
	 * @param color
	 * @return
	 * @author king
	 * @date 2025年6月12日-13:22:10
	 */
	public static Color color(int color) {
		return Color.rgb(color >> 16 & 0xFF, color >> 8 & 0xFF, color & 0xFF, (color >> 24 & 0xFF) / 255.0);
	}

}
